// Modified or written by Object Mentor, Inc. for inclusion with FitNesse.
// Copyright (c) 2002 Cunningham & Cunningham, Inc.
// Released under the terms of the GNU General Public License version 2 or later.package fit;

package fit;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import fit.exception.CouldNotParseFitFailureException;
import fit.exception.FitFailureException;
import fit.exception.FitMatcherException;
import fitlibrary.annotation.ShowSelectedActions;

// TODO-RcM Figure out how to make me smaller.
@ShowSelectedActions
public class Fixture {
	public Map<String, Object> summary = new HashMap<String, Object>();

	public Counts counts = new Counts();

	public FixtureListener listener = new NullFixtureListener();

	protected String[] args;

	private static Map<String, Object> symbols = new HashMap<String, Object>();
	private static boolean forcedAbort = false; // Semaphores

	public static void setForcedAbort(boolean state) {
		forcedAbort = state;
	} // Semaphores

	protected Class<?> getTargetClass() {
		return getClass();
	}

	public class RunTime {
		long start = System.currentTimeMillis();

		long elapsed = 0;

		@Override
		public String toString() {
			elapsed = System.currentTimeMillis() - start;
			if (elapsed > 600000)
				return d(3600000) + ":" + d(600000) + d(60000) + ":" + d(10000)
						+ d(1000);
			return d(60000) + ":" + d(10000) + d(1000) + "." + d(100) + d(10);
		}

		String d(long scale) {
			long report = elapsed / scale;
			elapsed -= report * scale;
			return Long.toString(report);
		}
	}

	// Traversal //////////////////////////

	/* Altered by Rick to dispatch on the first Fixture */

	public void doTables(Parse tables) {
		summary.put("run date", new Date());
		summary.put("run elapsed time", new RunTime());
		if (tables != null) {
			Parse heading = tables.at(0, 0, 0);
			if (heading != null) {
				try {
					Fixture fixture = getLinkedFixtureWithArgs(tables);
					fixture.listener = listener;
					fixture.interpretTables(tables);
				} catch (Throwable e) {
					exception(heading, e);
					interpretFollowingTables(tables);
				}
			}
		}
		listener.tablesFinished(counts);
		ClearSymbols();
		SemaphoreFixture.ClearSemaphores(); // Semaphores: clear all at end
	}

	public static void ClearSymbols() {
		symbols.clear();
	}

	/* Added by Rick to allow a dispatch into DoFixture */
	protected void interpretTables(Parse tables) {
		try { // Don't create the first fixture again, because creation may do
				// something important.
			getArgsForTable(tables); // get them again for the new fixture
										// object
			doTable(tables);
		} catch (Exception ex) {
			System.out.println("Fixture.interpretTables() " + ex);
			exception(tables.at(0, 0, 0), ex);
			listener.tableFinished(tables);
			return;
		}
		interpretFollowingTables(tables);
	}

	/* Added by Rick */
	private void interpretFollowingTables(Parse tablesInitial) {
		Parse tables = tablesInitial;
		listener.tableFinished(tables);
		tables = tables.more;
		while (tables != null) {
			Parse heading = tables.at(0, 0, 0);

			if (forcedAbort)
				ignore(heading); // Semaphores: ignore on failed lock
			else if (heading != null) {
				try {
					Fixture fixture = getLinkedFixtureWithArgs(tables);
					fixture.doTable(tables);
				} catch (Throwable e) {
					exception(heading, e);
				}
			}
			listener.tableFinished(tables);
			tables = tables.more;
		}
	}

	/* Added by Rick */
	protected Fixture getLinkedFixtureWithArgs(Parse tables) throws Throwable {
		Parse header = tables.at(0, 0, 0);
		Fixture fixture = loadFixture(header.text());
		fixture.counts = counts;
		fixture.summary = summary;
		fixture.getArgsForTable(tables);
		return fixture;
	}

	public static Fixture loadFixture(String fixtureName) throws Throwable {
		return FixtureLoader.instance().disgraceThenLoad(fixtureName);
	}

	public void getArgsForTable(Parse table) {
		List<String> argumentList = new ArrayList<String>();
		Parse parameters = table.parts.parts.more;
		for (; parameters != null; parameters = parameters.more)
			argumentList.add(parameters.text());

		args = argumentList.toArray(new String[0]);
	}

	public void doTable(Parse table) {
		doRows(table.parts.more);
	}

	public void doRows(Parse rowsInitial) {
		Parse rows = rowsInitial;
		while (rows != null) {
			Parse more = rows.more;
			doRow(rows);
			rows = more;
		}
	}

	public void doRow(Parse row) {
		doCells(row.parts);
	}

	public void doCells(Parse cellsInitial) {
		Parse cells = cellsInitial;
		for (int i = 0; cells != null; i++) {
			try {
				doCell(cells, i);
			} catch (Exception e) {
				exception(cells, e);
			}
			cells = cells.more;
		}
	}

	@SuppressWarnings("unused")
	public void doCell(Parse cell, int columnNumber) {
		ignore(cell);
	}

	// Annotation ///////////////////////////////

	public void right(Parse cell) {
		cell.addToTag(" class=\"pass\"");
		counts.right++;
	}

	public void wrong(Parse cell) {
		cell.addToTag(" class=\"fail\"");
		counts.wrong++;
	}

	public void wrong(Parse cell, String actual) {
		wrong(cell);
		cell.addToBody(label("expected") + "<hr>" + escape(actual)
				+ label("actual"));
	}

	public void ignore(Parse cell) {
		cell.addToTag(" class=\"ignore\"");
		counts.ignores++;
	}

	public void exception(Parse cell, Throwable exceptionInitial) {
		Throwable exception = exceptionInitial;
		while (exception.getClass().equals(InvocationTargetException.class)) {
			exception = ((InvocationTargetException) exception)
					.getTargetException();
		}
		if (isFriendlyException(exception)) {
			cell.addToBody("<hr/>" + label(exception.getMessage()));
		} else {
			final StringWriter buf = new StringWriter();
			exception.printStackTrace(new PrintWriter(buf));
			cell.addToBody("<hr><pre><div class=\"fit_stacktrace\">"
					+ (buf.toString()) + "</div></pre>");
		}
		cell.addToTag(" class=\"error\"");
		counts.exceptions++;
	}

	public boolean isFriendlyException(Throwable exception) {
		return exception instanceof FitFailureException;
	}

	// Utility //////////////////////////////////

	public String counts() {
		return counts.toString();
	}

	public static String label(String string) {
		return " <span class=\"fit_label\">" + string + "</span>";
	}

	public static String gray(String string) {
		return " <span class=\"fit_grey\">" + string + "</span>";
	}

	public static String escape(String string) {
		return escape(escape(string, '&', "&amp;"), '<', "&lt;");
	}

	public static String escape(String s, char from, String to) {
		String string = s;
		int i = -1;
		while ((i = string.indexOf(from, i + 1)) >= 0) {
			if (i == 0) {
				string = to + string.substring(1);
			} else if (i == string.length()) {
				string = string.substring(0, i) + to;
			} else {
				string = string.substring(0, i) + to + string.substring(i + 1);
			}
		}
		return string;
	}

	public static String camel(String name) {
		StringBuffer b = new StringBuffer(name.length());
		StringTokenizer t = new StringTokenizer(name);
		b.append(t.nextToken());
		while (t.hasMoreTokens()) {
			String token = t.nextToken();
			b.append(token.substring(0, 1).toUpperCase()); // replace spaces
															// with
			// camelCase
			b.append(token.substring(1));
		}
		return b.toString();
	}

	public Object parse(String s, Class<?> type) throws Exception {
		if (type.equals(String.class)) {
			if (s.toLowerCase().equals("null"))
				return null;
			else if (s.toLowerCase().equals("blank"))
				return "";
			else
				return s;
		} else if (type.equals(Date.class)) {
			return DateFormat.getDateInstance(DateFormat.SHORT).parse(s);
		} else if (hasParseMethod(type)) {
			return callParseMethod(type, s);
		} else {
			throw new CouldNotParseFitFailureException(s, type.getName());
		}
	}

	public void check(Parse cell, TypeAdapter a) {
		String text = cell.text();
		if (text.equals(""))
			handleBlankCell(cell, a);
		else if (a == null)
			ignore(cell);
		else if (text.equals("error"))
			handleErrorInCell(a, cell);
		else
			compareCellToResult(a, cell);
	}

	private void compareCellToResult(TypeAdapter a, Parse cell) {
		new CellComparator().compareCellToResult(a, cell);
	}

	public void handleBlankCell(Parse cell, TypeAdapter a) {
		try {
			cell.addToBody(gray(a.toString(a.get())));
		} catch (Exception e) {
			cell.addToBody(gray("error"));
		}
	}

	private void handleErrorInCell(TypeAdapter a, Parse cell) {
		try {
			Object result = a.invoke();
			wrong(cell, a.toString(result));
		} catch (IllegalAccessException e) {
			exception(cell, e);
		} catch (Exception e) {
			right(cell);
		}
	}

	public String[] getArgs() {
		return args;
	}

	public static void setSymbol(String name, Object value) {
		symbols.put(name, (value == null) ? "null" : value);
	}

	public static Object getSymbol(String name) {
		return symbols.get(name);
	}

	public static boolean hasSymbol(String name) {
		return symbols.containsKey(name);
	}

	public static boolean hasParseMethod(Class<?> type) {
		try {
			type.getMethod("parse", new Class<?>[] { String.class });
			return true;
		} catch (NoSuchMethodException e) {
			return false;
		}
	}

	public static Object callParseMethod(Class<?> type, String s)
			throws Exception {
		Method parseMethod = type.getMethod("parse",
				new Class<?>[] { String.class });
		Object o = parseMethod.invoke(null, new Object[] { s });
		return o;
	}

	// TODO-RcM I might be moving out of here. Can you help me find a home of my
	// own?
	class CellComparator {
		private Object result = null;

		private Object expected = null;

		private TypeAdapter typeAdapter;

		private Parse cell;

		public void compareCellToResult(TypeAdapter a, Parse theCell) {
			typeAdapter = a;
			cell = theCell;

			try {
				result = typeAdapter.get();
				expected = parseCell();
				if (expected instanceof Unparseable)
					tryRelationalMatch();
				else
					compare();
			} catch (Exception e) {
				exception(cell, e);
			}
		}

		private void compare() {
			if (typeAdapter.equals(expected, result)) {
				right(cell);
			} else {
				wrong(cell, typeAdapter.toString(result));
			}
		}

		private Object parseCell() {
			try {
				return typeAdapter.isRegex ? cell.text() : typeAdapter
						.parse(cell.text());
			}
			// Ignore parse exceptions, print non-parse exceptions,
			// return null so that compareCellToResult tries relational
			// matching.
			catch (NumberFormatException e) {
				//
			} catch (ParseException e) {
				//
			} catch (Exception e) {
				// e.printStackTrace();
			}
			return new Unparseable();
		}

		private void tryRelationalMatch() {
			Class<?> adapterType = typeAdapter.type;
			FitFailureException cantParseException = new CouldNotParseFitFailureException(
					cell.text(), adapterType.getName());
			if (result != null) {
				FitMatcher matcher = new FitMatcher(cell.text(), result);
				try {
					if (matcher.matches())
						right(cell);
					else
						wrong(cell);
					cell.body = matcher.message();
				} catch (FitMatcherException fme) {
					exception(cell, cantParseException);
				} catch (Exception e) {
					exception(cell, e);
				}
			} else {
				// TODO-RcM Is this always accurate?
				exception(cell, cantParseException);
			}
		}
	}

	protected class Unparseable {
		//
	}
}